# Progress meter
#
# Copyright (C) 2002-2003, 2005-2006, 2009 The Written Word, Inc.
# Copyright (C) 2001 The Written Word, LLC
# Copyright (C) 2003, 2005 Seth Vidal
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# $Id$

import time
import sys

# display text-based progress meter
class TextProgressMeter:
  def __init__ (self, indent, stream = sys.stdout):
    self.indent = ' ' * indent
    self.stream = stream
    self.update_period = 0.3		# 0.3 seconds

  def start (self, num_bytes):
    self.num_bytes = num_bytes
    self.start_time = 0
    self.last_update = 0

    self.num_read = 0
    self.now = self.last_update = time.time ()
    self.last_elapsed_time = 0
    self.last_read = 0
    self.ave_rate = None

  def end (self):
    self.now = time.time ()
    human_readable_time = self.format_time (self.now - self.start_time)
    human_readable_bytes = self.format_number (self.num_read)

    bar = '=' * 30
    _progress = '\r%s%3i%% |%-30.30s| %5sB %8s     ' % \
        (self.indent, 100, bar, human_readable_bytes, human_readable_time)

    self.stream.write (_progress + '\n')
    self.stream.flush ()

  def update (self, num_read):
    self.num_read = num_read		# for end ()

    _now = time.time ()
    if self.start_time == 0:
      self.start_time = _now            # set start time on first call

    if (_now >= self.last_update + self.update_period) or \
        not self.last_update:
      self.now = _now
      self._do_update (num_read)
      self.last_update = _now

  # status message while downloading a file
  def _do_update (self, num_read):
    # elapsed time since last update
    elapsed_time = self.now - self.start_time

    if self.num_bytes > 0:
      frac = float (num_read) / self.num_bytes
      if frac > 1.0:
        frac = 1.0
    else:
      self.start_time = time.time ()
      frac = 0.0

    bar = '=' * int (30 * frac)
    human_readable_bytes = self.format_number (num_read)
    human_readable_time = self.format_time (self.project (elapsed_time,
                                                          num_read))

    _progress = '\r%s%3i%% |%-30.30s| %5sB %8s ETA ' % \
      (self.indent, frac * 100, bar, human_readable_bytes, human_readable_time)
    self.stream.write (_progress)
    self.stream.flush ()

  # turn numbers into human-readable metric-like numbers
  def format_number (self, num_read):
    space = ' '
    step = 1024.0
    symbols = [ '',   # (none)
                'k',  # kilo
                'M',  # mega
                'G',  # giga
                'T',  # tera
                'P',  # peta
                'E',  # exa
                'Z',  # zetta
                'Y' ] # yotta

    threshold = 999
    depth = 0
    
    # we want numbers between 
    while num_read > threshold:
      depth  = depth + 1
      num_read = num_read / step

    # just in case someone needs more than 1000 yottabytes!
    diff = depth - len (symbols) + 1
    if diff > 0:
      depth = depth - diff
      num_read = num_read * threshold**depth

    if type (num_read) == type (1) or type (num_read) == type (1L):
      format = '%i%s%s'
    elif num_read < 9.95:
      # must use 9.95 for proper sizing.  For example, 9.99 will be
      # rounded to 10.0 with the .1f format string (which is too long)
      format = '%.1f%s%s'
    else:
      format = '%.0f%s%s'
    
    return (format % (num_read, space, symbols[depth]))

  def format_time (self, seconds):
    if seconds is None or seconds < 0:
      return '--:--'
    else:
      seconds = int (seconds)
      minutes = seconds / 60
      seconds = seconds % 60

      return '%02i:%02i' % (minutes, seconds)

  def project (self, elapsed_time, num_read):
    # get projected time for total download
    if num_read == 0:
      # if we just started this file, all bets are off
      self.last_elapsed_time = elapsed_time
      self.last_read  = 0
      self.ave_rate = None

      return None

    time_diff = elapsed_time - self.last_elapsed_time
    read_diff = num_read  - self.last_read
    self.last_elapsed_time = elapsed_time
    self.last_read  = num_read
    try:
      rate = time_diff / read_diff	# this is actually an inverse-rate
    except ZeroDivisionError:
      return 0				# should only happen at end of file

    self._get_new_ave_rate (rate)
    remaining_time = self.ave_rate * (self.num_bytes - num_read)
    if remaining_time < 0:
      remaining_time = 0

    return self._round_remaining_time (remaining_time)

  def _get_new_ave_rate (self, rate, epsilon = 0.98):
    if self.ave_rate == None:
      self.ave_rate = rate
    else:
      # calculate a "rolling average" - this balances long-term behavior
      # with short-term fluctuations
      # epsilon = 0.0  -->  only consider most recent block
      # epsilon = 1.0  -->  only consider first block
      self.ave_rate = (self.ave_rate * epsilon) + (rate * (1-epsilon))

  def _round_remaining_time (self, remaining_time):
    # round to further stabilize it
    i = 1
    while remaining_time > 30:
      i = i * 2
      remaining_time = remaining_time / 2
    remaining_time = int (remaining_time)

    return float (remaining_time * i)
